Skip to content

Linux 共享内存与内存映射 ​

管道每传一次数据都要经过内核中转,共享内存是 Linux 里最快的 IPC 方式:多个进程映射同一块物理内存,读写不经内核搬运。实现它有两条路——System V 共享内存(shmget 一族)和内存映射(mmap)。本文写给需要高频进程间数据交换的开发者,讲清两条路的接口、易错点与选择依据。

两条路的共同原理 ​

普通 IPC(管道、消息队列)需要「发送方从用户空间复制进内核、接收方再从内核复制回用户空间」两次拷贝;共享内存与 mmap(MAP_SHARED) 都把同一物理页装进各进程的地址空间,数据本身不动,天然没有这两次拷贝,这也是两者速度都快的原因。代价是需要自己解决同步(通常配合信号量使用)。

内存映射 mmap ​

mmap 把磁盘文件映射进内存:返回一个指针,写这块内存等于写文件(MAP_SHARED 模式下自动同步回磁盘)。

c
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// addr:传 NULL 由内核选择地址
// length:映射长度,不能为 0,建议取文件实际大小(stat/lseek 获取)
// prot:PROT_READ / PROT_WRITE / PROT_EXEC / PROT_NONE,
//       操作映射内存必须至少含 PROT_READ
// flags:MAP_SHARED(写回文件,IPC 必须用它)或
//        MAP_PRIVATE(写时复制,改动不影响原文件)
// fd:open 得到的文件描述符,文件大小不能为 0,
//     open 的权限不能与 prot 冲突(见下方易错点)
// offset:偏移量,必须是页大小(通常 4KB)的整数倍,0 表示从头映射
// 返回:成功返回映射区首地址,失败返回 MAP_FAILED((void *) -1)

int munmap(void *addr, size_t length);   // 释放映射,addr 必须是 mmap 的返回值

父子进程通信 ​

fork 之前建好映射区,子进程直接继承:

c
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>

int main() {
    int fd = open("test.txt", O_RDWR);          // 目标文件,需非空
    int size = lseek(fd, 0, SEEK_END);

    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    pid_t pid = fork();
    if (pid > 0) {
        wait(NULL);
        char buf[64];
        strcpy(buf, (char *)ptr);               // 父进程读到子进程写入的数据
        printf("read data: %s\n", buf);
    } else if (pid == 0) {
        strcpy((char *)ptr, "hello from child");
    }

    munmap(ptr, size);
    return 0;
}

无关进程通信则各自 open 同一个文件、各自 mmap,效果相同。注意 mmap 通信是非阻塞的:对面还没写入时读到的是映射区的当前内容,同步要靠应用层约定。

七个易错点 ​

  1. 对返回值 ptr 做 ptr++ 之后再 munmap(ptr, len) 会失败——释放必须传映射时的首地址,原值要留存
  2. open 用了 O_RDONLY,prot 却要 PROT_READ | PROT_WRITE:返回 MAP_FAILED。open 权限应与 prot 一致(prot 要写则 open 必须读写)
  3. offset 不是页大小整数倍:返回 MAP_FAILED(EINVAL)
  4. 其他失败情形:length 为 0;prot 只给 PROT_WRITE(不给读权限);prot 与 open 权限冲突
  5. 想映射 O_CREAT 新建的空文件:文件大小为 0 不行,先扩展(lseek 到末尾写一个字节,或 truncate())
  6. mmap 之后 close(fd):映射依然有效,close 不影响已建立的映射
  7. 只映射 100 字节却访问第 101 字节:越界访问非法内存,段错误

System V 共享内存 ​

System V shm 不依赖文件,直接向内核申请一段内存,用 key 标识:

c
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
// 创建或获取一个共享内存段,返回其 id
// key:非 0 标识(常用 ftok 生成);size:段大小(新建时有效,新建内容清零)
// shmflg:权限(如 0664)附加 IPC_CREAT(不存在则创建)、
//         IPC_CREAT | IPC_EXCL(已存在则报错,用于探测)
// 常见组合:IPC_CREAT | IPC_EXCL | 0664

void *shmat(int shmid, const void *shmaddr, int shmflg);
// 把段附加到本进程,返回首地址
// shmaddr 传 NULL 由内核选址;shmflg 传 0 读写、SHM_RDONLY 只读

int shmdt(const void *shmaddr);    // 分离(进程退出时自动分离)

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// cmd:IPC_STAT 读状态 / IPC_SET 设状态 / IPC_RMID 标记删除
// IPC_RMID 时 buf 传 NULL

key_t ftok(const char *pathname, int proj_id);
// 用一个存在的路径 + 8 位项目号(0-255,习惯传字符如 'a')生成 key

使用流程:shmget 创建/获取 → 两边 shmat 附加 → 读写 → shmdt 分离 → 一方 shmctl(IPC_RMID) 标记删除。

两个关键行为:

  • 删除是标记式的。IPC_RMID 之后段并不会立刻消失,要等所有附加进程都分离(shm_nattch 降为 0)才真正销毁;标记后 ipcs 里该段的 key 显示为 0(Linux 的实现行为,内核将其置为 IPC_PRIVATE;规范记载的标志是 shm_perm.mode 中的 SHM_DEST)。因此多次调用 IPC_RMID 是安全的
  • 进程退出会自动分离,但不会自动删除段——忘记 IPC_RMID 的段会一直留在内核里,重启前都能用 ipcs 看到

查看与清理命令 ​

shell
ipcs -a    # 所有 IPC 设施
ipcs -m    # 共享内存段
ipcs -q    # 消息队列
ipcs -s    # 信号量

ipcrm -m shmid      # 按 id 删除共享内存段
ipcrm -M shmkey     # 按 key 删除共享内存段
ipcrm -q msqid / -Q msgkey / -s semid / -S semkey   # 消息队列、信号量同理

两者怎么选 ​

维度 System V 共享内存 mmap 内存映射
依赖 不需要文件(匿名 IPC key 即可) 需要一个磁盘文件(匿名映射除外)
数据搬运 都不经内核拷贝,性能同级 同左
地址空间 所有进程附加同一块共享内存 每个进程有自己的虚拟映射,指向同一物理页
进程崩溃 段还在,数据可恢复访问 映射消失,但数据已在文件里
整机断电 内存数据丢失 文件内容还在(已同步的部分)
生命周期 显式 IPC_RMID 且全部分离后才销毁 进程退出映射即销毁

选择依据归纳:需要数据在进程崩溃后仍可访问、或顺便落盘,用 mmap 映射文件;纯粹的高性能临时共享、不想管理文件,用 System V shm(或 POSIX 的 shm_open + mmap,接口更现代)。无论哪条路,同步都要另配信号量或应用层协议。

要点 ​

  • 共享内存与 mmap(MAP_SHARED) 都免除了内核中转的两次拷贝,是最快的 IPC;同步需自理
  • mmap 五个硬约束:length 非 0、offset 页对齐、open 权限与 prot 一致、文件非空、munmap 用原首地址
  • System V shm 的 IPC_RMID 是标记删除,nattch 归 0 才真正销毁;进程退出只分离不删除
  • 遗留的段用 ipcs -m 查看、ipcrm -m 清理
  • 数据要落盘或跨崩溃存活选 mmap;纯内存高速共享选 shm

相关文章 ​

最近更新

基于 VitePress 构建